Skip to content

fix(executor): guard against missing content in provider responses#3952

Open
lawrence3699 wants to merge 1 commit intosimstudioai:mainfrom
lawrence3699:fix/executor-missing-content-guard
Open

fix(executor): guard against missing content in provider responses#3952
lawrence3699 wants to merge 1 commit intosimstudioai:mainfrom
lawrence3699:fix/executor-missing-content-guard

Conversation

@lawrence3699
Copy link
Copy Markdown

Problem

While reading through the executor handlers, I noticed that the router and evaluator handlers access result.content directly after response.json() without checking whether the field is present. If an LLM provider returns a 200 response but without a content field — for example when a model is temporarily unavailable, rate-limited mid-stream, or the provider returns an unexpected response structure — the handlers crash with:

TypeError: Cannot read properties of undefined (reading 'trim')

This surfaces as an opaque internal error to the user, with no indication of what went wrong.

Affected paths

  • RouterBlockHandler (legacy): result.content.trim().toLowerCase() at the routing decision step
  • RouterBlockHandler V2: result.content.trim() in the JSON parse fallback path — the try block catches the parse error, but the catch fallback also crashes if content is missing
  • EvaluatorBlockHandler: this.extractJSONFromResponse(result.content) passes undefined into extractJSONFromResponse, which then calls .trim() on it

Existing correct pattern

shared/response-format.ts (line 91) already handles this case:

const content = result.content ?? ''

The mothership handler also uses result.content || ''. The router and evaluator handlers were the only ones missing this guard.

Fix

Add an explicit !result.content check before accessing the field. When the content is missing or empty, the handler now throws a descriptive error message instead of an opaque TypeError.

Before → After

Before: Provider returns { model: "gpt-4o", tokens: {...} } (no content field) → TypeError: Cannot read properties of undefined (reading 'trim') → workflow fails with cryptic error

After: Same response → Error: Provider returned an empty response. The model may be unavailable or the request was malformed. → workflow fails with actionable error message

Tests added

  • router-handler.test.ts: 3 new tests covering legacy (missing content, empty string content) and V2 (missing content)
  • evaluator-handler.test.ts: 1 new test for missing content

All 33 handler tests pass.

When an LLM provider returns a 200 response without a content field,
the router and evaluator handlers crash with a TypeError trying to
call .trim() on undefined. This happens when a model is unavailable,
rate-limited mid-stream, or returns an unexpected response structure.

Add an explicit check for result.content before accessing it, matching
the existing pattern in shared/response-format.ts (line 91) which
already uses `result.content ?? ''` for this case.

Affected handlers:
- RouterBlockHandler (legacy path, line 127)
- RouterBlockHandler V2 (line 287 fallback after JSON parse failure)
- EvaluatorBlockHandler (line 148)

Added regression tests for all three paths.
Copilot AI review requested due to automatic review settings April 4, 2026 23:04
@cursor
Copy link
Copy Markdown

cursor bot commented Apr 4, 2026

PR Summary

Low Risk
Low risk: adds simple validation/error handling for empty provider responses and corresponding test coverage, without changing request formats or routing/evaluation logic.

Overview
Prevents RouterBlockHandler (legacy and V2) and EvaluatorBlockHandler from crashing when an LLM provider returns a 200 response without usable content by explicitly rejecting empty/missing result.content with a clearer, actionable error message.

Adds unit tests covering missing/empty provider content for router (legacy + V2) and evaluator handlers to ensure the new error behavior is enforced.

Reviewed by Cursor Bugbot for commit 177c6a5. Bugbot is set up for automated code reviews on this repo. Configure here.

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 4, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Apr 4, 2026 11:04pm

Request Review

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 4, 2026

Greptile Summary

This PR adds defensive !result.content guards in the router block handler (both legacy and V2 paths) and evaluator block handler to handle the case where an LLM provider returns a 200 response but without a content field. Previously, accessing result.content.trim() (or passing undefined into extractJSONFromResponse) would produce an opaque TypeError: Cannot read properties of undefined (reading 'trim'). Now all three affected paths throw the actionable error: "Provider returned an empty response. The model may be unavailable or the request was malformed." This is consistent with the existing result.content || '' pattern in the mothership handler and the result.content ?? '' fallback in shared/response-format.ts.

Key changes:

  • evaluator-handler.ts: Guard added at line 148 before this.extractJSONFromResponse(result.content)
  • router-handler.ts (legacy): Guard added at line 127 before result.content.trim().toLowerCase()
  • router-handler.ts (V2): Guard added at line 282 before JSON parsing — this also fixes the previously unsafe catch block fallback (result.content.trim()) which would crash identically if content was absent
  • 4 new tests covering missing-content and empty-string-content scenarios across both handler test files

Confidence Score: 5/5

Safe to merge — targeted defensive fix with no happy-path logic changes and good test coverage.

No P0 or P1 issues found. The single minor gap is that the router V2 test suite does not include an empty-string-content case (the legacy test suite does), but this is a P2 coverage observation and does not affect correctness. The fix is minimal, consistent with established codebase patterns, and the three new guard code paths are each covered by at least one test.

No files require special attention.

Important Files Changed

Filename Overview
apps/sim/executor/handlers/router/router-handler.ts Adds !result.content guards in both legacy (line 127) and V2 (line 282) paths; V2 catch-block fallback is now also safe since content is guaranteed truthy past the guard
apps/sim/executor/handlers/evaluator/evaluator-handler.ts Adds !result.content guard before extractJSONFromResponse; correctly prevents TypeError when provider response omits the content field
apps/sim/executor/handlers/router/router-handler.test.ts Adds 3 tests: legacy missing content, legacy empty-string content, and V2 missing content — all asserting the new descriptive error message
apps/sim/executor/handlers/evaluator/evaluator-handler.test.ts Adds 1 test validating the missing-content guard throws the expected descriptive error in the evaluator handler

Sequence Diagram

sequenceDiagram
    participant W as Workflow Engine
    participant H as Handler (Router / Evaluator)
    participant P as Provider API

    W->>H: execute(ctx, block, inputs)
    H->>P: POST /api/providers

    alt Provider returns no content field
        P-->>H: 200 OK { model, tokens } (content absent)
        Note over H: NEW: if (!result.content) guard
        H-->>W: throw Error("Provider returned an empty response...")
        Note over W: Workflow fails with actionable error
    else Provider returns valid content
        P-->>H: 200 OK { content: "...", model, tokens }
        H->>H: trim / parse / extractJSONFromResponse
        H-->>W: BlockOutput
    end
Loading

Reviews (1): Last reviewed commit: "fix(executor): guard against missing con..." | Re-trigger Greptile

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens executor handlers against provider responses that succeed (HTTP 200) but omit the expected content field, replacing runtime TypeError crashes with a descriptive error.

Changes:

  • Added explicit guards in Router (legacy + V2) and Evaluator handlers before using result.content.
  • Added/expanded unit tests to cover missing/empty provider content for Router and Evaluator handlers.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
apps/sim/executor/handlers/router/router-handler.ts Adds result.content presence checks before trimming/parsing routing decisions.
apps/sim/executor/handlers/router/router-handler.test.ts Adds tests for missing/empty content in legacy Router and missing content in Router V2.
apps/sim/executor/handlers/evaluator/evaluator-handler.ts Adds result.content presence check before JSON extraction/parsing.
apps/sim/executor/handlers/evaluator/evaluator-handler.test.ts Adds a test ensuring a clear error is thrown when content is missing.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +127 to 133
if (!result.content) {
throw new Error(
'Provider returned an empty response. The model may be unavailable or the request was malformed.'
)
}

const chosenBlockId = result.content.trim().toLowerCase()
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The guard if (!result.content) only catches missing/empty values, but it will still allow truthy non-string content (e.g., an object/array) through and then result.content.trim() will throw. It also treats whitespace-only strings as non-empty, leading to a later Invalid routing decision: error instead of the intended clear provider-empty error. Consider validating result is an object and typeof result.content === 'string' and that result.content.trim().length > 0 before calling .trim()/.toLowerCase().

Suggested change
if (!result.content) {
throw new Error(
'Provider returned an empty response. The model may be unavailable or the request was malformed.'
)
}
const chosenBlockId = result.content.trim().toLowerCase()
if (!result || typeof result !== 'object' || typeof result.content !== 'string') {
throw new Error(
'Provider returned an empty response. The model may be unavailable or the request was malformed.'
)
}
const normalizedContent = result.content.trim()
if (normalizedContent.length === 0) {
throw new Error(
'Provider returned an empty response. The model may be unavailable or the request was malformed.'
)
}
const chosenBlockId = normalizedContent.toLowerCase()

Copilot uses AI. Check for mistakes.
Comment on lines 280 to +287
const result = await response.json()

if (!result.content) {
throw new Error(
'Provider returned an empty response. The model may be unavailable or the request was malformed.'
)
}

Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as the legacy path: if (!result.content) doesn’t protect against truthy non-string values, and whitespace-only content will fall through to the JSON parse / fallback and eventually call .trim() anyway. Tighten the validation to require a non-empty trimmed string (and ideally guard result itself) before using result.content.

Copilot uses AI. Check for mistakes.
Comment on lines 146 to 154
const result = await response.json()

if (!result.content) {
throw new Error(
'Provider returned an empty response. The model may be unavailable or the request was malformed.'
)
}

const parsedContent = this.extractJSONFromResponse(result.content)
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (!result.content) won’t catch truthy non-string content, so extractJSONFromResponse(result.content) can still receive a non-string and crash when it calls .trim(). Also, whitespace-only strings slip through and will likely produce a confusing downstream parse/score behavior. Consider validating typeof result.content === 'string' and result.content.trim() before proceeding.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants